package controllers.sys;

import anna.Check;
import com.alibaba.fastjson.JSONObject;
import models.sys.AdminUser;
import packaged.SessionManager;
import play.Play;
import play.cache.Cache;
import play.data.validation.Required;
import play.libs.Images;
import play.mvc.Before;
import play.mvc.Controller;
import play.utils.Java;
import utils.RenderTemplate;
import utils.TextUtils;

import java.lang.reflect.InvocationTargetException;

public class Secure extends Controller {
    public static final String ADMIN_LOGIN_INAGE_VERFY_CODE = "ADMIN_LOGIN_INAGE_VERFY_CODE_";

    @Before(unless={"login", "authenticate", "logout", "getLoginImageCaptcha"})
    static void checkAccess() throws Throwable {


        if (SessionManager.sessionIsExpire(session)) {
            flash.put("url", "GET".equals(request.method) ? request.url : Play.ctxPath + "/"); // seems a good default
            login();
        } else {
            SessionManager.sessionIncreaseExpire(session);
        }

        // Checks
        Check check = getActionAnnotation(Check.class);
        if(check != null) {
            check(check);
        }
        check = getControllerInheritedAnnotation(Check.class);
        if(check != null) {
            check(check);
        }
    }

    private static void check(Check check) throws Throwable {
            boolean hasProfile = (Boolean) Security.invoke("check");
            if(!hasProfile) {
                Security.invoke("onCheckFailed");
        }
    }

    // ~~~ Login

    public static void login() {
        if (!SessionManager.sessionIsExpire(session))
            redirect("/");
        if (request.isAjax())
            renderJSON(RenderTemplate.needLogin());
        flash.keep("url");
        JSONObject loginInfo = new JSONObject();
        loginInfo.put("title",Play.configuration.getProperty("application.title"));
        render(loginInfo);
    }

    public static void getLoginImageCaptcha() {
        String request_ip = request.remoteAddress;  //获得请求者IP
        Images.Captcha captcha = Images.captcha();
        captcha.addNoise();
        Cache.set(ADMIN_LOGIN_INAGE_VERFY_CODE + request_ip,captcha.getText(),"5mn");
        renderBinary(captcha);
    }

    public static void authenticate(@Required String username, String password,String verification, boolean remember) throws Throwable {
        // Check tokens
        String request_ip = request.remoteAddress;  //获得请求者IP
        //IP登录次数限制KEY
        String ADMIN_LOGIN_PASS_ERROR_LOCK = "ADMIN_LOGIN_PASS_ERROR_LOCK_" + request_ip;
        //检查IP是否存在登陆限制，如果存在登陆驳回其登录请求
        if(Cache.get(ADMIN_LOGIN_PASS_ERROR_LOCK) != null){
            flash.keep("url");
            //返回登录限制状态
            flash.error(String.valueOf(RenderTemplate.ResponseCode.TOO_FREQUENTLY));
            params.flash();
            login();
        }
        //如果是debug登录模式，则不校验其验证码的合法性
        if (Play.mode.isProd()) {
            //根据IP取得其请求的验证码，再与参数中的验证码作比对，如果验证码不一致，则驳回其登录请求
            String saveVerification = TextUtils.stringOf(Cache.get(ADMIN_LOGIN_INAGE_VERFY_CODE + request_ip));
            //如果没有检测到其请求过验证码，则视为验证码超时，返回验证码超时信息
            if (saveVerification == null) {
                flash.keep("url");
                flash.error(String.valueOf(RenderTemplate.ResponseCode.VERIFYCODE_OVERTIME));
                params.flash();
                login();
            }
            //如果提交的验证码与存储的验证码不一致，则返回错误信息
            if (!saveVerification.equalsIgnoreCase(verification)) {
                flash.keep("url");
                flash.error(String.valueOf(RenderTemplate.ResponseCode.VERIFYCODE_ERROR));
                params.flash();
                login();
            }
        }
        //验证用户登录信息是否正确
        Boolean allowed = (Boolean) Security.invoke("authenticate", username, password, remember);
        //如果登录信息不正确，则对其失败的登录请求做记录，当请求次数超过5次，则锁定其IP15分析
        if(validation.hasErrors() || !allowed) {
            //IP错误次数记录Key
            String ADMIN_LOGIN_PASS_ERROR_COUNT = "ADMIN_LOGIN_PASS_ERROR_COUNT_" + request_ip;
            //允许错误的次数（实际-1）
            Integer errcount = 4;
            //获取上次的登录剩余次数
            if(Cache.get(ADMIN_LOGIN_PASS_ERROR_COUNT) != null){
                errcount = (Integer) Cache.get(ADMIN_LOGIN_PASS_ERROR_COUNT);
                if(errcount <= 1){
                    Cache.set(ADMIN_LOGIN_PASS_ERROR_LOCK,true,"15mn");
                    Cache.delete(ADMIN_LOGIN_PASS_ERROR_COUNT);
                }
            }
            flash.keep("url");
            flash.put("errcount",errcount);
            errcount --;
            Cache.set(ADMIN_LOGIN_PASS_ERROR_COUNT,errcount);
            flash.error(String.valueOf(RenderTemplate.ResponseCode.PASSWORD_ERROR));
            params.flash();
            login();
        }

        //设置用户登录信息
        AdminUser user = AdminUser.find("byUsername", username).first();
        SessionManager.setSession(request,response,session, user, remember);
        SessionManager.setAttribute(session, "uid", String.valueOf(user.id));
        if (user.role != null) {
            SessionManager.setAttribute(session, "rid", String.valueOf(user.role.id));
        }

        // Redirect to the original URL (or /)
        redirectToOriginalURL();
    }

    public static void logout() {
        try {
            Security.invoke("onDisconnect");
            SessionManager.removeSession(session);
            flash.success("secure.logout");
            Security.invoke("onDisconnected");
        } catch (Throwable throwable) {
            SessionManager.removeSession(session);
        }
        login();
    }

    // ~~~ Utils

    static void redirectToOriginalURL() throws Throwable {
        Security.invoke("onAuthenticated");
        String url = flash.get("url");
        if(url == null) {
            url = Play.ctxPath + "/admin";
        }
        redirect(url);
    }

    public static class Security extends Controller {

        /**
         * @Deprecated
         * 
         * @param username
         * @param password
         * @return
         */
        static boolean authentify(String username, String password, Boolean remember) {
            throw new UnsupportedOperationException();
        }

        /**
         * This method is called during the authentication process. This is where you check if
         * the user is allowed to log in into the system. This is the actual authentication process
         * against a third party system (most of the time a DB).
         *
         * @param username
         * @param password
         * @return true if the authentication process succeeded
         */
        static boolean authenticate(String username, String password, Boolean remember) {
            return true;
        }

        /**
         * This method checks that a profile is allowed to view this page/method. This method is called prior
         * to the method's controller annotated with the @Check method. 
         *
         * @return true if you are allowed to execute this controller method.
         */
        static boolean check() {
            return true;
        }

        /**
         * This method returns the current connected username
         * @return
         */
        static String connected() {
            return session.get("username");
        }

        /**
         * Indicate if a user is currently connected
         * @return  true if the user is connected
         */
        static boolean isConnected() {
            return session.contains("username");
        }

        /**
         * This method is called after a successful authentication.
         * You need to override this method if you with to perform specific actions (eg. Record the time the user signed in)
         */
        static void onAuthenticated() {
        }

         /**
         * This method is called before a user tries to sign off.
         * You need to override this method if you wish to perform specific actions (eg. Record the name of the user who signed off)
         */
        static void onDisconnect() {
        }

         /**
         * This method is called after a successful sign off.
         * You need to override this method if you wish to perform specific actions (eg. Record the time the user signed off)
         */
        static void onDisconnected() {
        }

        /**
         * This method is called if a check does not succeed. By default it shows the not allowed page (the controller forbidden method).
         */
        static void onCheckFailed() {
            forbidden();
        }

        private static Object invoke(String m, Object... args) throws Throwable {

            try {
                return Java.invokeChildOrStatic(Security.class, m, args);       
            } catch(InvocationTargetException e) {
                throw e.getTargetException();
            } catch (NullPointerException e){
                return null;
            }
        }

    }

}
